using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq.Expressions;
using System.Reflection;

namespace WindowsPhoneMVC.Navigation
{
    public class NavigationExpressionParser
    {
        private readonly MethodCallExpression body;

        public NavigationExpressionParser(MethodCallExpression body)
        {
            this.body = body;
            var method = body.Method;
            ActionName = method.Name;
            TargetMethodParameters = method.GetParameters();
            ExpressionArguments = body.Arguments;
        }

        public string ActionName { get; private set; }
        public ParameterInfo[] TargetMethodParameters { get; private set; }
        public ReadOnlyCollection<Expression> ExpressionArguments { get; private set; }

        public NavigationParameter[] GetNavigationParameters()
        {
            var result = new List<NavigationParameter>();
            for (var i = 0; i < TargetMethodParameters.Length && i < ExpressionArguments.Count; i++)
            {
                result.Add(GetNavigationParameter(TargetMethodParameters[i], ExpressionArguments[i]));
            }

            return result.ToArray();
        }

        private NavigationParameter GetNavigationParameter(ParameterInfo targetMethodParameter, Expression expressionArgument)
        {
            var parameterName = targetMethodParameter.Name;
            object value = null;

            var constant = expressionArgument as ConstantExpression;
            if (constant != null)
                value = GetConstantValue(constant);

            var property = expressionArgument as MemberExpression;
            if (property != null)
                value = GetMemberValue(property);

            if (constant == null && property == null)
                throw new InvalidOperationException(string.Format("Expression type {0} is not supported by Windows Phone MVC", expressionArgument.GetType().Name));

            return new NavigationParameter(parameterName, value);
        }

        private object GetMemberValue(MemberExpression property)
        {
            if (property.Expression is MemberExpression)
                return GetMemberValue((MemberExpression) property.Expression);
            object value;
            var memberInfo = property.Member;
            var prop = ((ConstantExpression)property.Expression).Value;

            if (memberInfo is PropertyInfo)
            {
                value = HandleMemberAccessExceptions((PropertyInfo)memberInfo, m => m.GetValue(prop, null));
            }
            else if (memberInfo is FieldInfo)
            {
                value = HandleMemberAccessExceptions((FieldInfo)memberInfo, f => f.GetValue(prop));
            }
            else if (memberInfo is MethodInfo)
            {
                value = HandleMemberAccessExceptions((MethodInfo)memberInfo, GetMethodValue);
            }
            else
                throw new NotImplementedException(string.Format("Sorry, we don't support Members of type {0} yet", memberInfo.GetType().Name));

            return value;
        }

        private object GetMethodValue(MethodInfo arg)
        {
            throw new NotSupportedException(string.Format(
                "Sorry, we don't support passing method values into navigation requests, use a property or this alternate syntax: "));
        }

        private object HandleMemberAccessExceptions<T>(T memberInfo, Func<T, object> getValue) where T : MemberInfo
        {
            try
            {
                return getValue(memberInfo);
            }
            catch (FieldAccessException ex)
            {
                throw AccessException<T>(memberInfo, "If this a local variable in a method, convert to a property or use the alternate syntax: {1}", ex);                    
            }
            catch (MemberAccessException ex)
            {
                throw AccessException<T>(memberInfo, "Either make {0} public or use the following method call instead {1}" + 
                    (typeof(T) == typeof(MethodInfo) ? "(...)" : string.Empty), ex);
            }
        }

        private NotSupportedException AccessException<T>(MemberInfo memberInfo, string error, Exception ex)
        {
            var firstLine = string.Format(
                    "Due to WP7 platform restrictions we cannot access the value of the {0} {1}",
                    typeof(T).Name.Replace("Info", string.Empty), memberInfo.Name);

            throw new NotSupportedException(string.Format("{0}\r\n\r\n{1}", firstLine, string.Format(error, memberInfo.Name, CalculateOverload())), ex);
        }

        private string CalculateOverload()
        {
            var arguments = new List<string>();

            foreach (var argument in ExpressionArguments)
            {
                if (argument is ConstantExpression)
                {
                    var value = ((ConstantExpression)argument).Value;
                    arguments.Add(value is string ? string.Format("\"{0}\"", value) : value.ToString());
                }
                if (argument is MemberExpression)
                {
                    arguments.Add(((MemberExpression)argument).Member.Name);
                    
                }
            }

            var parameterExpression = ((ParameterExpression)body.Object);
            return string.Format("Controller<{0}>().Invoke({1} => {1}.{2}, {3})", 
                parameterExpression == null ? "YourController" : parameterExpression.Type.Name,
                parameterExpression == null ? "c" : parameterExpression.Name, 
                ActionName, string.Join(", ", arguments.ToArray()));
        }

        private static object GetConstantValue(ConstantExpression constant)
        {
            return constant.Value;
        }
    }
}